#ifndef AER_PROGRAM_HPP_
#define AER_PROGRAM_HPP_

#include "aerDeviceResource.hpp"

#include <glm/gtc/type_ptr.hpp>
#include <glsw/glsw.h>//


namespace aer {

class Program : public DeviceResource
{
  public:
    enum ShaderType
    {
      VERTEX_SHADER           = GL_VERTEX_SHADER,
      TESS_CONTROL_SHADER     = GL_TESS_CONTROL_SHADER,
      TESS_EVALUATION_SHADER  = GL_TESS_EVALUATION_SHADER,
      GEOMETRY_SHADER         = GL_GEOMETRY_SHADER,
      FRAGMENT_SHADER         = GL_FRAGMENT_SHADER,
      
      COMPUTE_SHADER          = GL_COMPUTE_SHADER
    };
    
    
  private:
    static Program *sCurrent;
  
  
  public:
    static inline const Program* getCurrent()
    {
      AER_ASSERT( NULLPTR != sCurrent );//
      return sCurrent;
    }
    
  
  public:
    Program() : DeviceResource() {}
  
    void generate() 
    {
      AER_ASSERT( !isGenerated() );
      m_id = glCreateProgram();
    }
    
    void release() 
    {
      if (isGenerated()) 
      {
        glDeleteProgram(m_id); 
        m_id = 0u;
      }
    }
  
    void bind() 
    {
      AER_ASSERT( isGenerated() );
      sCurrent = this; 
      glUseProgram(m_id); 
    }
    
    static void unbind() 
    { 
      sCurrent = NULLPTR; 
      glUseProgram(0u); 
    }
    
    
    bool create( const ShaderType shaderType, const char* name);
    
    bool addShader( const ShaderType shaderType, const char* name);
    
    bool addShaderSrc( const ShaderType shaderType, const char* source, const char* name="");
    
    bool link();      

    inline GLint getUniformLocation(const char* name) const 
    {
      GLint loc = glGetUniformLocation( m_id, name);
#     ifndef NDEBUG
      if (loc < 0) { 
        fprintf( stderr, "[DEBUG] : invalid uniform name \"%s\".\n", name);
        exit(1);
      }
#     endif
      return loc;
    }
    

    // ===============  
    // + Set Uniform +
    // ===============
    inline void setUniform(const char* name, const GLint v) const
    {
      glUniform1i( getUniformLocation(name), v);
    }
    
    inline void setUniform(const char* name, const GLuint v) const
    {
      glUniform1ui( getUniformLocation(name), v);
    }
    
    inline void setUniform(const char* name, const GLfloat v) const
    {
      glUniform1f( getUniformLocation(name), v);
    }
    
    inline void setUniform(const char* name, const float v0, const float v1) const 
    {
      glUniform2f( getUniformLocation(name), v0, v1);
    }
    
    inline void setUniform(const char* name, const float v0, const float v1, 
                           const float v2) const
    {
      glUniform3f( getUniformLocation(name), v0, v1, v2);
    }
    
    inline void setUniform(const char* name, const float v0, const float v1, 
                           const float v2, const float v3) const
    {
      glUniform4f( getUniformLocation(name), v0, v1, v2, v3);
    }
  
    inline void setUniform(const char* name, const glm::vec2 &v) const 
    {
      glUniform2fv( getUniformLocation(name), 1, glm::value_ptr(v));
    }
    
    inline void setUniform(const char* name, const glm::vec3 &v) const 
    {
      glUniform3fv( getUniformLocation(name), 1, glm::value_ptr(v));
    }
    
    inline void setUniform(const char* name, const glm::vec4 &v) const 
    {
      glUniform4fv( getUniformLocation(name), 1, glm::value_ptr(v));
    }
    
  
    // =======================
    // + set Matrix uniforms +
    // =======================
    inline void setUniform(const char* name, const glm::mat3 &v) const 
    { 
      glUniformMatrix3fv( getUniformLocation(name), 1, GL_FALSE, glm::value_ptr(v));
    }  
    
    inline void setUniform(const char* name, const glm::mat4 &v) const 
    { 
      glUniformMatrix4fv( getUniformLocation(name), 1, GL_FALSE, glm::value_ptr(v));
    }

    // =======================
    // + set Arrays uniforms +
    // =======================
    inline void setUniform(const char* name, const GLint *v, int count) const 
    {
      glUniform1iv( getUniformLocation(name), count, v);
    }
    
    inline void setUniform(const char* name, const GLuint *v, int count) const 
    {
      glUniform1uiv( getUniformLocation(name), count, v);
    }
    
    inline void setUniform(const char* name, const GLfloat *v, int count) const 
    {
      glUniform1fv( getUniformLocation(name), count, v);
    }
    
    inline void setUniform(const char* name, const glm::vec2 *v, int count) const 
    {
      glUniform2fv( getUniformLocation(name), count, glm::value_ptr(*v));
    }
    
    inline void setUniform(const char* name, const glm::vec3 *v, int count) const 
    {
      glUniform3fv( getUniformLocation(name), count, glm::value_ptr(*v));
    }  
    
    inline void setUniform(const char* name, const glm::vec4 *v, int count) const 
    {
      glUniform4fv( getUniformLocation(name), count, glm::value_ptr(*v));
    }
    
    inline void setUniform(const char* name, const glm::mat3 *v, int count) const 
    {
      glUniformMatrix3fv( getUniformLocation(name), count, GL_FALSE, glm::value_ptr(*v));
    }
    
    inline void setUniform(const char* name, const glm::mat4 *v, int count) const 
    {
      glUniformMatrix4fv( getUniformLocation(name), count, GL_FALSE, glm::value_ptr(*v));
    }

    
   private:
    DISALLOW_COPY_AND_ASSIGN(Program);
};

} // aer

#endif // AER_PROGRAM_HPP_

/** 
 ------------------------------------------------------------------------------
 
aer::Program program;

program.generate();
  program.addShader( VERTEX_SHADER,   "PassThrough.Vertex");
  program.addShader( FRAGMENT_SHADER, "PassThrough.Fragment");
program.link();

program.bind();
  program.setUniform( "uModelViewProjMatrix", mvpMatrix);
  drawObject();
program.unbind();

program.release();

 ------------------------------------------------------------------------------
*/
